package info.niwota.ziplock;

import info.niwota.ziplock.FileUtils.FileVisitor;
import info.niwota.ziplock.zip.CryptoUtils;
import info.niwota.ziplock.zip.ZipUtils;

import java.io.Closeable;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.Key;
import java.util.Collections;
import java.util.List;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;

/**
 * @author qiangli
 * 
 */
public class FileContentProvider extends ContentProvider {
	/**
	 * A wrapper to remove temp file when done
	 */
	private static class TempParcelFileDescriptor extends ParcelFileDescriptor {
		private java.io.File temp = null;

		public TempParcelFileDescriptor(ParcelFileDescriptor fd,
				java.io.File temp) {
			super(fd);
			this.temp = temp;
		}

		@Override
		public void close() throws IOException {
			super.close();
			temp.delete();
			//
			if (Debug.DEBUG) {
				Log.d(TAG, "closed " + temp);
			}
		}

	}

	public static final String AUTHORITY = "info.niwota.ziplock.file";

	private static final int BUFSIZE = 1024;

	public static final Uri CONTENT_URI = Uri
			.parse("content://info.niwota.ziplock.file");

	public static final String FILE_EXT = ".out";

	public static final File ROOT = new File(SDCard.getRoot(), "cache");

	private static long seq = System.currentTimeMillis();

	private static final String TAG = "FileContentProvider";

	private static String tmpprefix = FileContentProvider.class.getSimpleName();

	public static void closeSilently(Closeable c) {
		if (c == null) {
			return;
		}
		try {
			c.close();
		} catch (Throwable t) {
			// do nothing
		}
	}

	public static Uri constructUri(Uri uri) {
		Uri u = Uri.parse(CONTENT_URI + uri.getEncodedPath());
		if (Debug.DEBUG) {
			Log.d(TAG, " " + u);
		}
		return u;
	}

	private static void copy(InputStream is, OutputStream os)
			throws IOException {
		try {
			byte[] buf = new byte[BUFSIZE];
			while (true) {
				int read = is.read(buf);
				if (read == -1) {
					break;
				}
				if (read > 0) {
					os.write(buf, 0, read);
				}
			}
		} finally {
			closeSilently(is);
			closeSilently(os);
		}
	}

	private static java.io.File createTempFile(File cacheDir)
			throws IOException {
		java.io.File f = new java.io.File(cacheDir, nextTempName());
		f.deleteOnExit();
		return f;
	}

	private static ParcelFileDescriptor createTempParcelFileDescriptor(
			java.io.File temp) throws FileNotFoundException {
		ParcelFileDescriptor fd = ParcelFileDescriptor.open(temp,
				ParcelFileDescriptor.MODE_READ_ONLY);
		return new TempParcelFileDescriptor(fd, temp);
	}

	private static String[] getZipName(String name) {
		// format: zip/name
		// return name without leading or trailing slash
		String[] na = new String[2];
		int idx = name.indexOf("/");
		na[0] = name.substring(0, idx);
		na[1] = name.substring(idx + 1);
		return na;
	}

	private static InputStream makeInputStream(File file, Key key) {
		try {
			if (Debug.DEBUG) {
				Log.d(TAG, "makeInputStream " + file);
			}
			InputStream is = CryptoUtils.openInputStream(file, key);
			return is;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	synchronized private static String nextTempName() {
		seq++;
		return tmpprefix + seq + ".tmp";
	}

	public static ParcelFileDescriptor open(Uri uri, String mode, File cache)
			throws FileNotFoundException {

		if (Debug.DEBUG) {
			Log.d(TAG, "*** open: " + uri + " mode: " + mode);
		}

		try {
			String p = uri.getPath();
			if (p.startsWith("/lock/")) {
				String name = p.substring(6);
				File f = new File(ROOT, name + FILE_EXT);
				String[] na = getZipName(name);
				File zip = new File(FolderContentProvider.ZIP_BASE, na[0]);
				Key key = KeyStore.getKey();
				if (key != null) {
					boolean exists = f.exists();
					if (Debug.DEBUG) {
						Log.d(TAG, "exists? " + exists + " zipfile: " + zip
								+ " file: " + f + " zip: " + na[0] + " name: "
								+ na[1]);
					}
					if (!exists) {
						CryptoUtils.extractFile(zip, f, key, key, na[1]);
					}
					InputStream is = makeInputStream(f, key);
					if (is != null) {
						File temp = createTempFile(cache);
						File dir = temp.getParentFile();
						if (!dir.exists()) {
							dir.mkdirs();
						}
						OutputStream os = new FileOutputStream(temp);
						copy(is, os);
						return createTempParcelFileDescriptor(temp);
					}
				}
			} else if (p.startsWith("/zip/")) {
				String name = p.substring(5);
				File f = new File(ROOT, name + FILE_EXT);
				String[] na = getZipName(name);
				File zip = new File(FolderContentProvider.ZIP_BASE, na[0]);
				boolean exists = f.exists();
				if (Debug.DEBUG) {
					Log.d(TAG, "exists? " + exists + " zipfile: " + zip
							+ " file: " + f + " zip: " + na[0] + " name: "
							+ na[1]);
				}
				if (!exists) {
					ZipUtils.extractFile(zip, f, na[1]);
				}
				return ParcelFileDescriptor.open(f,
						ParcelFileDescriptor.MODE_READ_ONLY);
			}
		} catch (Exception e) {
			if (Debug.DEBUG) {
				// exception expected if not logged in
				// e.printStackTrace();
				Log.d(TAG, "Exception: " + e.getMessage());
			}
		}

		throw new FileNotFoundException(uri + "");
	}

	private File cacheDir;

	@Override
	public void attachInfo(Context context, ProviderInfo info) {
		super.attachInfo(context, info);
	}

	@Override
	public int bulkInsert(Uri uri, ContentValues[] values) {
		throw new UnsupportedOperationException();
	}

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		throw new UnsupportedOperationException();
	}

	@Override
	public String getType(Uri uri) {
		// String mimetype = uri.getQueryParameter("mimetype");
		String name = uri.getLastPathSegment();
		if (Debug.DEBUG) {
			Log.d(TAG, "query: " + name + " uri: " + uri);
		}
		return null;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		throw new UnsupportedOperationException();
	}

	@Override
	protected boolean isTemporary() {
		return false;
	}

	@Override
	public boolean onCreate() {
		try {
			Context ctx = getContext();
			this.cacheDir = new File(ctx.getCacheDir(), this.getClass()
					.getName());
			this.cacheDir.mkdirs();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return true;
	}

	@Override
	public AssetFileDescriptor openAssetFile(Uri uri, String mode)
			throws FileNotFoundException {
		ParcelFileDescriptor fd = openFile(uri, mode);
		return (fd != null ? new AssetFileDescriptor(fd, 0, -1) : null);
	}

	@Override
	public ParcelFileDescriptor openFile(Uri uri, String mode)
			throws FileNotFoundException {
		return open(uri, mode, cacheDir);
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		throw new UnsupportedOperationException();
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		throw new UnsupportedOperationException();
	}

	public static void load(Uri uri, StatusMonitor status,
			final List<FileItem> list) throws Exception {

		if (Debug.DEBUG) {
			Log.d(TAG, "uri: " + uri);
		}
		final File zipfile = new File(uri.getPath());
		load(zipfile, status, list);
		//

		Collections.sort(list);
	}

	private static void load(File zipfile, StatusMonitor status,
			final List<FileItem> files) throws Exception {
		// top: /root/cache/zip/path/bame/
		long start = System.currentTimeMillis();
		if (Debug.DEBUG) {
			Log.d(TAG, "load start listing... ");
		}
		final String zipname = zipfile.getName();
		final boolean locked = zipfile.getName().endsWith(".zip.enc");
		final File top = new File(FileContentProvider.ROOT, zipname);
		//
		boolean isCached = (top.exists() && top.lastModified() > zipfile
				.lastModified());
		//
		if (isCached) {
			if (Debug.DEBUG) {
				Log.d(TAG, "returning cached content");
			}
			final int from = top.getPath().length() + 1;
			FileUtils.walk(top, new FileFilter() {

				public boolean accept(File pathname) {
					return (pathname.isFile());
				}

			}, new FileVisitor() {

				public void doAccept(File file) {
					String n = file.getPath().substring(from);
					n = n.substring(0, n.length() - 4); // remove .out
					FileItem fi = new FileItem(n);
					fi.locked = locked;
					files.add(fi);
				}

				public void onFile(File file) {
				}

				public void onFolder(File dir) {
				}

			}, status);
			return;
		}
		// reload
		if (Debug.DEBUG) {
			Log.d(TAG, "reloading content...");
		}
		String ext = FileContentProvider.FILE_EXT;
		if (locked) {
			Key key = KeyStore.getKey();
			CryptoUtils.unzip(top, zipfile, key, key, ext, status, files);
		} else {
			ZipUtils.unzip(top, zipfile, ext, status, files);
		}

		long end = System.currentTimeMillis();

		if (Debug.DEBUG) {
			Log.d(TAG, "load done " + ((end - start) / 1000) + " list size: "
					+ (files.size()));
		}
	}

	public static Uri constructBaseUri(String zipname) {
		boolean locked = zipname.endsWith(".zip.enc");
		String zl = (locked ? "/lock/" : "/zip/");
		// 
		Uri base = Uri.parse(CONTENT_URI + zl + zipname);
		return base;
	}

	public static Uri constructUri(Uri zip, String p) {
		String zipname = zip.getLastPathSegment();
		Uri base = constructBaseUri(zipname);
		Uri uri = Uri.withAppendedPath(base, p);
		return uri;
	}
}
